@scrabble-solver/scrabble-solver 2.18.1 → 2.18.3

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 (196) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +14 -14
  3. package/.next/cache/.previewinfo +1 -1
  4. package/.next/cache/.rscinfo +1 -1
  5. package/.next/cache/.tsbuildinfo +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/client-production/index.pack.old +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  11. package/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  14. package/.next/diagnostics/framework.json +1 -1
  15. package/.next/dynamic-css-manifest.json +2 -2
  16. package/.next/prerender-manifest.json +4 -4
  17. package/.next/react-loadable-manifest.json +37 -37
  18. package/.next/routes-manifest.json +1 -1
  19. package/.next/server/chunks/309.js +1 -1
  20. package/.next/server/chunks/344.js +1 -1
  21. package/.next/server/chunks/371.js +1 -1
  22. package/.next/server/chunks/392.js +1 -1
  23. package/.next/server/chunks/465.js +1 -1
  24. package/.next/server/chunks/546.js +1 -1
  25. package/.next/server/chunks/641.js +1 -1
  26. package/.next/server/chunks/67.js +1 -0
  27. package/.next/server/chunks/712.js +1 -1
  28. package/.next/server/chunks/812.js +1 -1
  29. package/.next/server/chunks/846.js +1 -1
  30. package/.next/server/chunks/886.js +1 -1
  31. package/.next/server/chunks/942.js +1 -0
  32. package/.next/server/chunks/953.js +1 -1
  33. package/.next/server/chunks/961.js +1 -1
  34. package/.next/server/dynamic-css-manifest.js +1 -1
  35. package/.next/server/middleware-build-manifest.js +1 -1
  36. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  37. package/.next/server/pages/404.html +3 -3
  38. package/.next/server/pages/500.html +3 -3
  39. package/.next/server/pages/_app.js +1 -1
  40. package/.next/server/pages/_app.js.nft.json +1 -1
  41. package/.next/server/pages/_document.js +2 -2
  42. package/.next/server/pages/_document.js.nft.json +1 -1
  43. package/.next/server/pages/_error.js +1 -1
  44. package/.next/server/pages/_error.js.nft.json +1 -1
  45. package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  46. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  47. package/.next/server/pages/api/dictionary/[locale].js +1 -1
  48. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  49. package/.next/server/pages/api/solve.js +1 -1
  50. package/.next/server/pages/api/solve.js.nft.json +1 -1
  51. package/.next/server/pages/api/verify.js +1 -1
  52. package/.next/server/pages/api/verify.js.nft.json +1 -1
  53. package/.next/server/pages/index.html +3 -3
  54. package/.next/server/pages/index.js +4 -4
  55. package/.next/server/pages/index.js.nft.json +1 -1
  56. package/.next/server/pages/index.json +1 -1
  57. package/.next/server/pages/not-found.html +3 -3
  58. package/.next/server/pages/not-found.js.nft.json +1 -1
  59. package/.next/server/pages-manifest.json +1 -1
  60. package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_buildManifest.js +1 -1
  61. package/.next/static/chunks/100.305bdf9c75b1758f.js +1 -0
  62. package/.next/static/chunks/256.6a20b23a766cfebd.js +1 -0
  63. package/.next/static/chunks/{307.67f52f13e3d60d4e.js → 307.89e6100f58443679.js} +1 -1
  64. package/.next/static/chunks/{363.01eed3233f92b6a4.js → 363.cec65b1f9668a75e.js} +1 -1
  65. package/.next/static/chunks/{422.1559ae6756912ca5.js → 422.3e55b4c94e328701.js} +1 -1
  66. package/.next/static/chunks/{663.d2c9ca36b26695b8.js → 663.bd2c45413fbca518.js} +1 -1
  67. package/.next/static/chunks/693.677c88e0eb1340b5.js +1 -0
  68. package/.next/static/chunks/{820.97b82f751ca49579.js → 820.823e9e885ebb66af.js} +1 -1
  69. package/.next/static/chunks/827.c81b92b383a748c9.js +1 -0
  70. package/.next/static/chunks/{865.a9b563a45848a83c.js → 865.59baba8296c9b116.js} +1 -1
  71. package/.next/static/chunks/912.077603e4f0bc6cca.js +1 -0
  72. package/.next/static/chunks/{939.19b33a3d3c351d73.js → 939.2ad86b50405c7897.js} +1 -1
  73. package/.next/static/chunks/980.7ce17464fc56648a.js +1 -0
  74. package/.next/static/chunks/{main-b92e08f3fc96dd8d.js → main-7e52da35a4beea0b.js} +1 -1
  75. package/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +1 -0
  76. package/.next/static/chunks/pages/index-b073a1686818dd26.js +1 -0
  77. package/.next/static/chunks/webpack-d7138f1ef6b8c9d6.js +1 -0
  78. package/.next/static/css/65bcc9440e067113.css +1 -0
  79. package/.next/static/css/{4dbcacc1122ac6b9.css → 92f85ce358a0a468.css} +1 -1
  80. package/.next/static/css/a691348b788f8bc0.css +1 -0
  81. package/.next/static/css/cd4d7f2458487399.css +1 -0
  82. package/.next/trace +22 -21
  83. package/.next/trace-build +1 -1
  84. package/AGENTS.md +9 -0
  85. package/CLAUDE.md +1 -0
  86. package/package.json +14 -12
  87. package/public/llms.txt +1 -1
  88. package/src/components/Board/Board.tsx +3 -1
  89. package/src/components/Board/BoardPure.tsx +26 -20
  90. package/src/components/Board/components/Cell/Cell.tsx +4 -4
  91. package/src/components/Board/lib.test.ts +106 -0
  92. package/src/components/Board/lib.ts +34 -3
  93. package/src/components/Board/selectors.ts +17 -5
  94. package/src/components/Button/Button.module.scss +1 -1
  95. package/src/components/Modal/Modal.module.scss +0 -4
  96. package/src/components/Results/Header.tsx +30 -5
  97. package/src/components/Results/Result.tsx +45 -55
  98. package/src/components/Results/Results.module.scss +0 -126
  99. package/src/components/Results/Results.module.scss.d.ts +0 -10
  100. package/src/components/Results/Results.tsx +25 -5
  101. package/src/components/Solver/Solver.module.scss +4 -4
  102. package/src/components/Solver/Solver.tsx +5 -3
  103. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +5 -1
  104. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +1 -0
  105. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -2
  106. package/src/components/Table/Cell.tsx +42 -0
  107. package/src/components/Table/Header.tsx +11 -0
  108. package/src/components/{Results → Table}/HeaderButton.tsx +22 -14
  109. package/src/components/Table/Row.tsx +59 -0
  110. package/src/components/{ResultsInput/ResultsInput.tsx → Table/Search.tsx} +9 -11
  111. package/src/components/Table/Table.module.scss +147 -0
  112. package/src/components/Table/Table.module.scss.d.ts +19 -0
  113. package/src/components/Table/index.ts +5 -0
  114. package/src/components/Tile/Tile.module.scss +2 -1
  115. package/src/hooks/useColumns.ts +7 -5
  116. package/src/hooks/useIsCompactLayout.ts +14 -0
  117. package/src/i18n/languages/english.json +6 -1
  118. package/src/i18n/languages/french.json +6 -1
  119. package/src/i18n/languages/german.json +6 -1
  120. package/src/i18n/languages/persian.json +6 -1
  121. package/src/i18n/languages/polish.json +6 -1
  122. package/src/i18n/languages/romanian.json +6 -1
  123. package/src/i18n/languages/spanish.json +6 -1
  124. package/src/i18n/languages/turkish.json +6 -1
  125. package/src/icons/QuestionSquare.svg +5 -0
  126. package/src/lib/createCoordinatesComparator.ts +23 -0
  127. package/src/lib/createKeyComparator.ts +1 -4
  128. package/src/lib/createSortComparator.ts +22 -0
  129. package/src/lib/getCoordinates.test.ts +21 -0
  130. package/src/lib/getCoordinates.ts +16 -0
  131. package/src/lib/getNextSort.ts +12 -0
  132. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +3 -1
  133. package/src/modals/RemainingTilesModal/components/Character/Character.tsx +8 -9
  134. package/src/modals/ResultsModal/ResultsModal.tsx +14 -10
  135. package/src/modals/WordsModal/WordsModal.module.scss +16 -27
  136. package/src/modals/WordsModal/WordsModal.module.scss.d.ts +3 -6
  137. package/src/modals/WordsModal/WordsModal.tsx +56 -42
  138. package/src/modals/WordsModal/components/WordsTable/WordRow.tsx +92 -0
  139. package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss +69 -0
  140. package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss.d.ts +16 -0
  141. package/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +137 -0
  142. package/src/modals/WordsModal/components/WordsTable/index.ts +1 -0
  143. package/src/modals/WordsModal/components/index.ts +1 -0
  144. package/src/pages/api/verify.ts +7 -6
  145. package/src/pages/index.tsx +29 -4
  146. package/src/parameters/index.ts +2 -0
  147. package/src/sdk/verify.ts +3 -2
  148. package/src/solver-worker/client.test.ts +10 -3
  149. package/src/solver-worker/index.worker.ts +7 -7
  150. package/src/solver-worker/messages.ts +3 -3
  151. package/src/state/hoveredWord/index.ts +4 -0
  152. package/src/state/hoveredWord/initialState.ts +5 -0
  153. package/src/state/hoveredWord/selectors.ts +5 -0
  154. package/src/state/hoveredWord/slice.ts +18 -0
  155. package/src/state/hoveredWord/types.ts +5 -0
  156. package/src/state/index.ts +1 -0
  157. package/src/state/localStorage.ts +13 -2
  158. package/src/state/results/lib.ts +9 -26
  159. package/src/state/results/selectors.ts +9 -1
  160. package/src/state/results/slice.ts +4 -16
  161. package/src/state/sagas.ts +26 -1
  162. package/src/state/selectors.ts +18 -1
  163. package/src/state/store.ts +2 -0
  164. package/src/state/types.ts +2 -0
  165. package/src/state/verify/initialState.ts +7 -0
  166. package/src/state/verify/lib.test.ts +55 -0
  167. package/src/state/verify/lib.ts +49 -0
  168. package/src/state/verify/selectors.ts +37 -0
  169. package/src/state/verify/slice.ts +13 -2
  170. package/src/state/verify/types.ts +6 -2
  171. package/src/styles/_tokens.scss +2 -0
  172. package/src/styles/mixins.scss +5 -0
  173. package/src/styles/variables.module.scss +1 -0
  174. package/src/styles/variables.module.scss.d.ts +1 -0
  175. package/src/styles/variables.scss +1 -1
  176. package/src/types/index.ts +24 -11
  177. package/tsconfig.tsbuildinfo +1 -1
  178. package/.next/server/chunks/316.js +0 -1
  179. package/.next/server/chunks/916.js +0 -1
  180. package/.next/static/chunks/106.e414e88694add27f.js +0 -1
  181. package/.next/static/chunks/127.125d896a19061d71.js +0 -1
  182. package/.next/static/chunks/18.8b91e137d239e15c.js +0 -1
  183. package/.next/static/chunks/583.7a46807be14c377e.js +0 -1
  184. package/.next/static/chunks/711.ba7b8f900dfd40d0.js +0 -1
  185. package/.next/static/chunks/912.ff8037ba9d4db5c8.js +0 -1
  186. package/.next/static/chunks/pages/_app-2e607dcd4135f340.js +0 -1
  187. package/.next/static/chunks/pages/index-e0087247c995328d.js +0 -1
  188. package/.next/static/chunks/webpack-cfb34d175e081981.js +0 -1
  189. package/.next/static/css/b74cd7de82dabcdd.css +0 -1
  190. package/.next/static/css/c403ce739a9c0d83.css +0 -1
  191. package/.next/static/css/fbfdce70731d0d2b.css +0 -1
  192. package/src/components/Results/Cell.tsx +0 -32
  193. package/src/components/ResultsInput/ResultsInput.module.scss +0 -12
  194. package/src/components/ResultsInput/ResultsInput.module.scss.d.ts +0 -7
  195. package/src/components/ResultsInput/index.ts +0 -1
  196. /package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_ssgManifest.js +0 -0
@@ -0,0 +1,59 @@
1
+ import classNames from 'classnames';
2
+ import {
3
+ type CSSProperties,
4
+ type FocusEventHandler,
5
+ type FunctionComponent,
6
+ type MouseEventHandler,
7
+ type ReactNode,
8
+ } from 'react';
9
+
10
+ import styles from './Table.module.scss';
11
+
12
+ interface Props {
13
+ 'aria-label': string;
14
+ children: ReactNode;
15
+ className?: string;
16
+ 'data-testid'?: string;
17
+ highlighted: boolean;
18
+ inactive: boolean;
19
+ style?: CSSProperties;
20
+ onBlur?: FocusEventHandler;
21
+ onClick?: MouseEventHandler;
22
+ onFocus?: FocusEventHandler;
23
+ onMouseEnter?: MouseEventHandler;
24
+ }
25
+
26
+ export const Row: FunctionComponent<Props> = ({
27
+ 'aria-label': ariaLabel,
28
+ children,
29
+ className,
30
+ 'data-testid': dataTestId,
31
+ highlighted,
32
+ inactive,
33
+ style,
34
+ onBlur,
35
+ onClick,
36
+ onFocus,
37
+ onMouseEnter,
38
+ }) => {
39
+ return (
40
+ <button
41
+ aria-current={highlighted ? 'true' : undefined}
42
+ aria-hidden={inactive ? 'true' : undefined}
43
+ aria-label={ariaLabel}
44
+ className={classNames(styles.row, className, {
45
+ [styles.clickable]: Boolean(onClick),
46
+ [styles.highlighted]: highlighted,
47
+ })}
48
+ data-testid={dataTestId}
49
+ style={style}
50
+ type="button"
51
+ onBlur={onBlur}
52
+ onClick={onClick}
53
+ onFocus={onFocus}
54
+ onMouseEnter={onMouseEnter}
55
+ >
56
+ <span className={styles.rowContent}>{children}</span>
57
+ </button>
58
+ );
59
+ };
@@ -1,20 +1,18 @@
1
1
  import classNames from 'classnames';
2
2
  import { type ChangeEvent, type FunctionComponent, type SubmitEventHandler, useState } from 'react';
3
- import { useDispatch } from 'react-redux';
4
3
 
5
4
  import { isRegExp } from '@/lib/isRegExp';
6
- import { resultsSlice, selectResultsQuery, useTranslate, useTypedSelector } from '@/state';
7
5
 
8
- import styles from './ResultsInput.module.scss';
6
+ import styles from './Table.module.scss';
9
7
 
10
8
  interface Props {
11
9
  className?: string;
10
+ placeholder: string;
11
+ value: string;
12
+ onChange: (value: string) => void;
12
13
  }
13
14
 
14
- export const ResultsInput: FunctionComponent<Props> = ({ className }) => {
15
- const dispatch = useDispatch();
16
- const translate = useTranslate();
17
- const value = useTypedSelector(selectResultsQuery);
15
+ export const Search: FunctionComponent<Props> = ({ className, placeholder, value, onChange }) => {
18
16
  const [localValue, setLocalValue] = useState(value);
19
17
 
20
18
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
@@ -23,7 +21,7 @@ export const ResultsInput: FunctionComponent<Props> = ({ className }) => {
23
21
  setLocalValue(newValue);
24
22
 
25
23
  if (isRegExp(newValue)) {
26
- dispatch(resultsSlice.actions.changeQuery(newValue));
24
+ onChange(newValue);
27
25
  }
28
26
  };
29
27
 
@@ -32,10 +30,10 @@ export const ResultsInput: FunctionComponent<Props> = ({ className }) => {
32
30
  };
33
31
 
34
32
  return (
35
- <form className={classNames(styles.resultsInput, className)} onSubmit={handleSubmit}>
33
+ <form className={classNames(styles.search, className)} onSubmit={handleSubmit}>
36
34
  <input
37
- className={styles.input}
38
- placeholder={translate('results.input.placeholder')}
35
+ className={styles.searchField}
36
+ placeholder={placeholder}
39
37
  type="text"
40
38
  value={localValue}
41
39
  onChange={handleChange}
@@ -0,0 +1,147 @@
1
+ @use 'styles/mixins' as *;
2
+
3
+ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
4
+
5
+ .header {
6
+ position: relative;
7
+ z-index: 1;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ border-bottom: var(--border);
12
+ border-top-left-radius: inherit;
13
+ border-top-right-radius: inherit;
14
+ }
15
+
16
+ .headerButton {
17
+ @include button-reset;
18
+ @include focus-effect;
19
+
20
+ cursor: pointer;
21
+
22
+ text-transform: uppercase;
23
+ transition:
24
+ background-color var(--transition--duration) var(--transition--easing),
25
+ color var(--transition--duration) var(--transition--easing);
26
+ background-color: var(--color--background);
27
+ height: 100%;
28
+
29
+ &:first-child {
30
+ border-start-start-radius: inherit;
31
+ }
32
+
33
+ &:last-child {
34
+ border-start-end-radius: inherit;
35
+ }
36
+
37
+ &:focus,
38
+ &:hover {
39
+ background-color: var(--color--primary);
40
+ color: var(--color--primary--opposite);
41
+ }
42
+
43
+ &.primary {
44
+ flex: 1 1 0;
45
+ min-width: 0;
46
+ text-transform: uppercase;
47
+ }
48
+ }
49
+
50
+ .headerButtonLabel {
51
+ @include ellipsis;
52
+
53
+ flex: 0 1 auto;
54
+ text-align: start;
55
+ }
56
+
57
+ .headerButtonIcon {
58
+ flex: 0 0 auto;
59
+ width: var(--results--icon--size);
60
+ height: var(--results--icon--size);
61
+ }
62
+
63
+ .row {
64
+ @include button-reset;
65
+
66
+ user-select: none;
67
+ font-weight: 400;
68
+
69
+ /**
70
+ * :focus-visible rather than :focus - a row left focused by click/hover on another row
71
+ * must not keep looking highlighted.
72
+ */
73
+ &:focus-visible,
74
+ &:hover,
75
+ &.highlighted {
76
+ &:not(:disabled) {
77
+ background-color: var(--color--primary);
78
+ color: var(--color--primary--opposite);
79
+ }
80
+ }
81
+
82
+ &[aria-hidden='true'] {
83
+ color: var(--color--inactive);
84
+
85
+ &:focus-visible,
86
+ &:hover,
87
+ &.highlighted {
88
+ &:not(:disabled) {
89
+ background-color: var(--color--primary--light);
90
+ color: var(--color--primary--opposite);
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ .clickable {
97
+ cursor: pointer;
98
+ }
99
+
100
+ .rowContent {
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: space-between;
104
+ height: 100%;
105
+ }
106
+
107
+ .cell {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ height: 100%;
112
+ gap: var(--spacing--s);
113
+ line-height: var(--results--item--height);
114
+
115
+ &.primary {
116
+ flex: 1 1 0;
117
+ min-width: 0;
118
+ text-transform: uppercase;
119
+ justify-content: flex-start;
120
+ padding-inline-start: $row-padding-horizontal;
121
+ }
122
+ }
123
+
124
+ .cellValue {
125
+ @include ellipsis;
126
+
127
+ min-width: 0;
128
+ }
129
+
130
+ .search {
131
+ @include focus-effect;
132
+ }
133
+
134
+ .searchField {
135
+ @include text-input;
136
+
137
+ border: none;
138
+ border-radius: inherit;
139
+ }
140
+
141
+ .sortIcon {
142
+ $size: 19px;
143
+
144
+ flex: 0 0 auto;
145
+ width: $size;
146
+ height: $size;
147
+ }
@@ -0,0 +1,19 @@
1
+ // Generated by scripts/generate-scss-types.ts - do not edit.
2
+ declare const styles: {
3
+ readonly cell: string;
4
+ readonly cellValue: string;
5
+ readonly clickable: string;
6
+ readonly header: string;
7
+ readonly headerButton: string;
8
+ readonly headerButtonIcon: string;
9
+ readonly headerButtonLabel: string;
10
+ readonly highlighted: string;
11
+ readonly primary: string;
12
+ readonly row: string;
13
+ readonly rowContent: string;
14
+ readonly search: string;
15
+ readonly searchField: string;
16
+ readonly sortIcon: string;
17
+ };
18
+
19
+ export default styles;
@@ -0,0 +1,5 @@
1
+ export { Cell } from './Cell';
2
+ export { Header } from './Header';
3
+ export { HeaderButton } from './HeaderButton';
4
+ export { Row } from './Row';
5
+ export { Search } from './Search';
@@ -85,9 +85,10 @@
85
85
  caret-color: transparent;
86
86
  font-size: 16px; // prevent iOS from automatically zooming in on focus
87
87
  outline: none;
88
+ user-select: none;
88
89
 
89
90
  &::selection {
90
- --background--color: transparent;
91
+ background-color: transparent;
91
92
  }
92
93
  }
93
94
 
@@ -5,6 +5,7 @@ import { LOCALE_FEATURES } from '@/i18n/constants';
5
5
  import { selectLocale, useTypedSelector } from '@/state';
6
6
  import { ResultColumnId } from '@/types';
7
7
 
8
+ import { useIsCompactLayout } from './useIsCompactLayout';
8
9
  import { useMediaQueries } from './useMediaQueries';
9
10
 
10
11
  const COLUMNS_XS = [ResultColumnId.Coordinates, ResultColumnId.Word, ResultColumnId.Points];
@@ -13,7 +14,7 @@ const COLUMNS_S = [...COLUMNS_XS, ResultColumnId.BlanksCount, ResultColumnId.Wor
13
14
 
14
15
  const COLUMNS_M = [...COLUMNS_XS];
15
16
 
16
- const COLUMNS_L = [...COLUMNS_XS];
17
+ const COLUMNS_COMPACT = [...COLUMNS_XS];
17
18
 
18
19
  const selectColumns = createSelector([selectLocale], (locale) => {
19
20
  const { consonants, vowels } = LOCALE_FEATURES[locale];
@@ -39,7 +40,8 @@ const selectColumns = createSelector([selectLocale], (locale) => {
39
40
 
40
41
  export const useColumns = (): Partial<Record<ResultColumnId, boolean>> => {
41
42
  const columns = useTypedSelector(selectColumns);
42
- const { isLessThanXs, isLessThanS, isLessThanM, isLessThanL } = useMediaQueries();
43
+ const { isLessThanXs, isLessThanS, isLessThanM } = useMediaQueries();
44
+ const isCompactLayout = useIsCompactLayout();
43
45
 
44
46
  const filteredColumns = useMemo(() => {
45
47
  if (isLessThanXs) {
@@ -54,12 +56,12 @@ export const useColumns = (): Partial<Record<ResultColumnId, boolean>> => {
54
56
  return columns.filter((columnId) => COLUMNS_M.includes(columnId));
55
57
  }
56
58
 
57
- if (isLessThanL) {
58
- return columns.filter((columnId) => COLUMNS_L.includes(columnId));
59
+ if (isCompactLayout) {
60
+ return columns.filter((columnId) => COLUMNS_COMPACT.includes(columnId));
59
61
  }
60
62
 
61
63
  return columns;
62
- }, [columns, isLessThanXs, isLessThanS, isLessThanM, isLessThanL]);
64
+ }, [columns, isLessThanXs, isLessThanS, isLessThanM, isCompactLayout]);
63
65
 
64
66
  const columnsMap = useMemo(() => {
65
67
  return Object.fromEntries(filteredColumns.map((column) => [column, true]));
@@ -0,0 +1,14 @@
1
+ import { BREAKPOINT_HEIGHT_L } from '@/parameters';
2
+
3
+ import { useMedia } from './useMedia';
4
+ import { useMediaQuery } from './useMediaQuery';
5
+
6
+ const SHORT_VIEWPORT = `(max-height: ${BREAKPOINT_HEIGHT_L - 1}px)`;
7
+
8
+ // Keep in sync with the `compact` media expression in styles/mixins.scss
9
+ export function useIsCompactLayout(): boolean {
10
+ const isNarrowViewport = useMediaQuery('<l');
11
+ const isShortViewport = useMedia(SHORT_VIEWPORT);
12
+
13
+ return isNarrowViewport || isShortViewport;
14
+ }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Search results... (RegExp)",
60
60
  "results.insert": "Insert",
61
61
  "results.preview": "Preview",
62
+ "results.select": "Select...",
62
63
  "results.solve": "Solve",
63
64
  "settings": "Settings",
64
65
  "settings.autoGroupTiles": "Group remaining tiles",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternative",
79
80
  "settings.showCoordinates.original": "Original",
80
81
  "words": "Created words",
82
+ "words.empty-state.no-words": "No created words - the board is empty.",
83
+ "words.input.placeholder": "Search words... (RegExp)",
81
84
  "words.invalid": "Invalid",
82
- "words.valid": "Valid"
85
+ "words.preview": "Preview",
86
+ "words.valid": "Valid",
87
+ "words.validity": "Validity"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Rechercher les résultats... (RegExp)",
60
60
  "results.insert": "Inserer",
61
61
  "results.preview": "Prévisualisation",
62
+ "results.select": "Sélectionner...",
62
63
  "results.solve": "Résoudre",
63
64
  "settings": "Options",
64
65
  "settings.autoGroupTiles": "Grouper les cases restantes",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternative",
79
80
  "settings.showCoordinates.original": "Original",
80
81
  "words": "Mots créés",
82
+ "words.empty-state.no-words": "Pas de mots créés - le plateau est vide.",
83
+ "words.input.placeholder": "Rechercher les mots... (RegExp)",
81
84
  "words.invalid": "Incorrect",
82
- "words.valid": "Corriger"
85
+ "words.preview": "Prévisualisation",
86
+ "words.valid": "Corriger",
87
+ "words.validity": "Validité"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Suchergebnisse... (RegExp)",
60
60
  "results.insert": "Hinzufügen",
61
61
  "results.preview": "Vorschau",
62
+ "results.select": "Auswählen...",
62
63
  "results.solve": "Lösen",
63
64
  "settings": "Einstellungen",
64
65
  "settings.autoGroupTiles": "Restliche Steine gruppieren",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternative",
79
80
  "settings.showCoordinates.original": "Original",
80
81
  "words": "Gebildete Wörter",
82
+ "words.empty-state.no-words": "Keine gebildeten Wörter - das Brett ist leer.",
83
+ "words.input.placeholder": "Wörter suchen... (RegExp)",
81
84
  "words.invalid": "Falsch",
82
- "words.valid": "Korrekt"
85
+ "words.preview": "Vorschau",
86
+ "words.valid": "Korrekt",
87
+ "words.validity": "Gültigkeit"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "جستجو در نتایج (RegExp)",
60
60
  "results.insert": "وارد کردن",
61
61
  "results.preview": "پیش نمایش",
62
+ "results.select": "انتخاب کنید...",
62
63
  "results.solve": "حل کن",
63
64
  "settings": "تنظیمات",
64
65
  "settings.autoGroupTiles": "کاشی های باقی مانده ی طاقچه را کنار هم قرار بده",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "بديل",
79
80
  "settings.showCoordinates.original": "إبداعي",
80
81
  "words": "کلمات ساخته شده",
82
+ "words.empty-state.no-words": "کلمه‌ای ساخته نشده - صفحه خالی است.",
83
+ "words.input.placeholder": "جستجو در کلمات (RegExp)",
81
84
  "words.invalid": "نا معتبر",
82
- "words.valid": "معتبر"
85
+ "words.preview": "پیش نمایش",
86
+ "words.valid": "معتبر",
87
+ "words.validity": "اعتبار"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Szukaj rozwiązania... (RegExp)",
60
60
  "results.insert": "Wstaw",
61
61
  "results.preview": "Podgląd",
62
+ "results.select": "Wybierz...",
62
63
  "results.solve": "Rozwiąż",
63
64
  "settings": "Opcje",
64
65
  "settings.autoGroupTiles": "Grupuj pozostałe płytki",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternatywne",
79
80
  "settings.showCoordinates.original": "Oryginalne",
80
81
  "words": "Utworzone słowa",
82
+ "words.empty-state.no-words": "Brak utworzonych słów - plansza jest pusta.",
83
+ "words.input.placeholder": "Szukaj słów... (RegExp)",
81
84
  "words.invalid": "Niepoprawne",
82
- "words.valid": "Poprawne"
85
+ "words.preview": "Podgląd",
86
+ "words.valid": "Poprawne",
87
+ "words.validity": "Poprawność"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Cautare solutii... (RegExp)",
60
60
  "results.insert": "Introducere",
61
61
  "results.preview": "Previzualizare",
62
+ "results.select": "Selectati...",
62
63
  "results.solve": "Gasire solutii",
63
64
  "settings": "Setari",
64
65
  "settings.autoGroupTiles": "Litere ramase",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternativă",
79
80
  "settings.showCoordinates.original": "Original",
80
81
  "words": "Cuvinte create",
82
+ "words.empty-state.no-words": "Niciun cuvânt creat - tabla este goală.",
83
+ "words.input.placeholder": "Cautare cuvinte... (RegExp)",
81
84
  "words.invalid": "Invalid",
82
- "words.valid": "Valid"
85
+ "words.preview": "Previzualizare",
86
+ "words.valid": "Valid",
87
+ "words.validity": "Validitate"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Busque una solución... (RegExp)",
60
60
  "results.insert": "Insertar",
61
61
  "results.preview": "Vista previa",
62
+ "results.select": "Seleccionar...",
62
63
  "results.solve": "Resolver",
63
64
  "settings": "Configuración",
64
65
  "settings.autoGroupTiles": "Agrupar casillas restantes",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternativa",
79
80
  "settings.showCoordinates.original": "Original",
80
81
  "words": "Palabras creadas",
82
+ "words.empty-state.no-words": "No hay palabras creadas; el tablero está vacío.",
83
+ "words.input.placeholder": "Busque una palabra... (RegExp)",
81
84
  "words.invalid": "Incorrecto",
82
- "words.valid": "Correcto"
85
+ "words.preview": "Vista previa",
86
+ "words.valid": "Correcto",
87
+ "words.validity": "Validez"
83
88
  }
@@ -59,6 +59,7 @@
59
59
  "results.input.placeholder": "Sonuçları Ara... (RegExp)",
60
60
  "results.insert": "Sokmak",
61
61
  "results.preview": "Önizleme",
62
+ "results.select": "Seç...",
62
63
  "results.solve": "Çöz",
63
64
  "settings": "Ayarlar",
64
65
  "settings.autoGroupTiles": "Kalan taşları grupla",
@@ -78,6 +79,10 @@
78
79
  "settings.showCoordinates.alternative": "Alternatif",
79
80
  "settings.showCoordinates.original": "Orijinal",
80
81
  "words": "Oluşturulan kelimeler",
82
+ "words.empty-state.no-words": "Oluşturulan kelime yok - tahta boş.",
83
+ "words.input.placeholder": "Kelimeleri Ara... (RegExp)",
81
84
  "words.invalid": "Geçersiz",
82
- "words.valid": "Geçerli"
85
+ "words.preview": "Önizleme",
86
+ "words.valid": "Geçerli",
87
+ "words.validity": "Geçerlilik"
83
88
  }
@@ -0,0 +1,5 @@
1
+ <!-- https://icons.getbootstrap.com/icons/question-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-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" fill="currentColor" />
4
+ <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286m1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94" fill="currentColor" />
5
+ </svg>
@@ -0,0 +1,23 @@
1
+ import { type ShowCoordinates } from '@scrabble-solver/types';
2
+
3
+ import { type Comparator } from '@/types';
4
+
5
+ import { createStringComparator } from './createStringComparator';
6
+
7
+ interface Parameters<Item> {
8
+ getItemCoordinates: (item: Item, showCoordinates: ShowCoordinates) => string;
9
+ locale: string;
10
+ showCoordinates: ShowCoordinates;
11
+ }
12
+
13
+ export function createCoordinatesComparator<Item>({
14
+ getItemCoordinates,
15
+ locale,
16
+ showCoordinates,
17
+ }: Parameters<Item>): Comparator<Item> {
18
+ const stringComparator = createStringComparator(locale);
19
+
20
+ return (a, b) => {
21
+ return stringComparator(getItemCoordinates(a, showCoordinates), getItemCoordinates(b, showCoordinates));
22
+ };
23
+ }
@@ -3,10 +3,7 @@ import { type Comparator } from '@/types';
3
3
  import { createStringComparator } from './createStringComparator';
4
4
  import { numberComparator } from './numberComparator';
5
5
 
6
- export const createKeyComparator = <T extends Record<keyof T, unknown>>(
7
- key: keyof T,
8
- locale: string,
9
- ): Comparator<T> => {
6
+ export const createKeyComparator = <T extends object>(key: keyof T, locale: string): Comparator<T> => {
10
7
  const stringComparator = createStringComparator(locale);
11
8
 
12
9
  return (a: T, b: T): number => {
@@ -0,0 +1,22 @@
1
+ import { type ShowCoordinates } from '@scrabble-solver/types';
2
+
3
+ import { type Comparator, type ComparatorFactory, type Sort, SortDirection } from '@/types';
4
+
5
+ import { reverseComparator } from './reverseComparator';
6
+
7
+ interface Parameters<Item, Id extends string> {
8
+ comparators: Record<Id, ComparatorFactory<Item>>;
9
+ locale: string;
10
+ showCoordinates: ShowCoordinates;
11
+ sort: Sort<Id>;
12
+ }
13
+
14
+ export function createSortComparator<Item, Id extends string>({
15
+ comparators,
16
+ locale,
17
+ showCoordinates,
18
+ sort,
19
+ }: Parameters<Item, Id>): Comparator<Item> {
20
+ const comparator = comparators[sort.column](locale, showCoordinates);
21
+ return sort.direction === SortDirection.Descending ? reverseComparator(comparator) : comparator;
22
+ }
@@ -0,0 +1,21 @@
1
+ import { getCoordinates } from './getCoordinates';
2
+
3
+ describe('getCoordinates', () => {
4
+ const point = { x: 3, y: 5 };
5
+
6
+ it('encodes a horizontal word as row number followed by column letter in original mode', () => {
7
+ expect(getCoordinates({ ...point, isHorizontal: true }, 'original')).toBe('6D');
8
+ });
9
+
10
+ it('encodes a vertical word as column letter followed by row number in original mode', () => {
11
+ expect(getCoordinates({ ...point, isHorizontal: false }, 'original')).toBe('D6');
12
+ });
13
+
14
+ it('encodes a horizontal word as row letter followed by column number in alternative mode', () => {
15
+ expect(getCoordinates({ ...point, isHorizontal: true }, 'alternative')).toBe('F4');
16
+ });
17
+
18
+ it('encodes a vertical word as column number followed by row letter in alternative mode', () => {
19
+ expect(getCoordinates({ ...point, isHorizontal: false }, 'alternative')).toBe('4F');
20
+ });
21
+ });
@@ -0,0 +1,16 @@
1
+ import { type ShowCoordinates } from '@scrabble-solver/types';
2
+
3
+ import { getCoordinate } from './getCoordinate';
4
+
5
+ interface Parameters {
6
+ isHorizontal: boolean;
7
+ x: number;
8
+ y: number;
9
+ }
10
+
11
+ export const getCoordinates = ({ isHorizontal, x, y }: Parameters, showCoordinates: ShowCoordinates): string => {
12
+ const xCoordinate = getCoordinate(x, showCoordinates === 'original' ? 'letter' : 'number');
13
+ const yCoordinate = getCoordinate(y, showCoordinates === 'original' ? 'number' : 'letter');
14
+
15
+ return isHorizontal ? `${yCoordinate}${xCoordinate}` : `${xCoordinate}${yCoordinate}`;
16
+ };
@@ -0,0 +1,12 @@
1
+ import { type Sort, SortDirection } from '@/types';
2
+
3
+ export const getNextSort = <Id extends string>(sort: Sort<Id>, column: Id): Sort<Id> => {
4
+ return {
5
+ column,
6
+ direction: sort.column === column ? toggleDirection(sort.direction) : sort.direction,
7
+ };
8
+ };
9
+
10
+ const toggleDirection = (direction: SortDirection): SortDirection => {
11
+ return direction === SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending;
12
+ };
@@ -2,6 +2,7 @@ import { type FunctionComponent, memo } from 'react';
2
2
 
3
3
  import { Badge } from '@/components/Badge';
4
4
  import { Modal } from '@/components/Modal';
5
+ import { useMediaQuery } from '@/hooks/useMediaQuery';
5
6
  import { LOCALE_FEATURES } from '@/i18n/constants';
6
7
  import { selectLocale, useTranslate, useTypedSelector } from '@/state';
7
8
 
@@ -17,6 +18,7 @@ interface Props {
17
18
 
18
19
  const RemainingTilesModalBase: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
19
20
  const translate = useTranslate();
21
+ const coversViewport = useMediaQuery('<s');
20
22
  const locale = useTypedSelector(selectLocale);
21
23
  const groups = useTypedSelector(selectRemainingTilesGroups);
22
24
  const { direction } = LOCALE_FEATURES[locale];
@@ -44,7 +46,7 @@ const RemainingTilesModalBase: FunctionComponent<Props> = ({ className, isOpen,
44
46
  {tiles.map((tile) => {
45
47
  return (
46
48
  <div className={styles.character} key={tile.character}>
47
- <Character tile={tile} />
49
+ <Character canHighlight={!coversViewport} tile={tile} />
48
50
  </div>
49
51
  );
50
52
  })}