@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
@@ -1,11 +1,10 @@
1
- import classNames from 'classnames';
2
- import { type FocusEventHandler, type MouseEventHandler, type ReactElement, useRef } from 'react';
1
+ import { type FocusEventHandler, type MouseEventHandler, type ReactElement } from 'react';
3
2
  import Highlighter from 'react-highlight-words';
4
3
  import { type RowComponentProps } from 'react-window';
5
4
 
5
+ import { Cell, Row } from '@/components/Table';
6
6
  import { useColumns } from '@/hooks/useColumns';
7
7
  import { LOCALE_FEATURES } from '@/i18n/constants';
8
- import { noop } from '@/lib/noop';
9
8
  import {
10
9
  selectIsResultMatching,
11
10
  selectLocale,
@@ -15,7 +14,6 @@ import {
15
14
  } from '@/state';
16
15
  import { ResultColumnId } from '@/types';
17
16
 
18
- import { Cell } from './Cell';
19
17
  import styles from './Results.module.scss';
20
18
  import { type ResultData } from './types';
21
19
 
@@ -23,13 +21,12 @@ export const Result = ({
23
21
  index,
24
22
  highlightedIndex,
25
23
  results = [],
26
- onBlur = noop,
27
- onClick = noop,
28
- onFocus = noop,
29
- onMouseEnter = noop,
24
+ onBlur,
25
+ onClick,
26
+ onFocus,
27
+ onMouseEnter,
30
28
  style,
31
29
  }: RowComponentProps<ResultData>): ReactElement => {
32
- const ref = useRef<HTMLButtonElement>(null);
33
30
  const columns = useColumns();
34
31
  const locale = useTypedSelector(selectLocale);
35
32
  const query = useTypedSelector(selectResultsQuery);
@@ -39,68 +36,61 @@ export const Result = ({
39
36
  const words = direction === 'rtl' ? [...result.words].reverse() : result.words;
40
37
  const coordinates = useTypedSelector((state) => selectResultCoordinates(state, index));
41
38
 
42
- const handleClick: MouseEventHandler = (event) => onClick(result, event);
43
- const handleMouseEnter: MouseEventHandler = (event) => onMouseEnter(result, event);
44
- const handleBlur: FocusEventHandler = (event) => onBlur(result, event);
45
- const handleFocus: FocusEventHandler = (event) => onFocus(result, event);
39
+ const handleClick: MouseEventHandler | undefined = onClick && ((event) => onClick(result, event));
40
+ const handleMouseEnter: MouseEventHandler | undefined = onMouseEnter && ((event) => onMouseEnter(result, event));
41
+ const handleBlur: FocusEventHandler | undefined = onBlur && ((event) => onBlur(result, event));
42
+ const handleFocus: FocusEventHandler | undefined = onFocus && ((event) => onFocus(result, event));
46
43
 
47
44
  return (
48
- <button
49
- aria-current={index === highlightedIndex ? 'true' : undefined}
50
- aria-hidden={isMatching ? undefined : 'true'}
45
+ <Row
51
46
  aria-label={result.word}
52
- className={classNames(styles.result, {
53
- [styles.highlighted]: index === highlightedIndex,
54
- })}
55
47
  data-testid="result"
56
- ref={ref}
48
+ highlighted={index === highlightedIndex}
49
+ inactive={!isMatching}
57
50
  style={style}
58
- type="button"
59
51
  onBlur={handleBlur}
60
52
  onClick={handleClick}
61
53
  onFocus={handleFocus}
62
54
  onMouseEnter={handleMouseEnter}
63
55
  >
64
- <span className={styles.resultContent}>
65
- {columns[ResultColumnId.Coordinates] && (
66
- <Cell className={styles.coordinates} translationKey="settings.showCoordinates" value={coordinates} />
67
- )}
56
+ {columns[ResultColumnId.Coordinates] && (
57
+ <Cell className={styles.coordinates} translationKey="settings.showCoordinates" value={coordinates} />
58
+ )}
68
59
 
69
- {columns[ResultColumnId.Word] && (
70
- <Cell className={styles.word} translationKey="common.word" value={result.word}>
71
- <Highlighter highlightClassName={styles.highlight} searchWords={[query]} textToHighlight={result.word} />
72
- </Cell>
73
- )}
60
+ {columns[ResultColumnId.Word] && (
61
+ <Cell primary translationKey="common.word" value={result.word}>
62
+ <Highlighter highlightClassName={styles.highlight} searchWords={[query]} textToHighlight={result.word} />
63
+ </Cell>
64
+ )}
74
65
 
75
- {columns[ResultColumnId.TilesCount] && (
76
- <Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
77
- )}
66
+ {columns[ResultColumnId.TilesCount] && (
67
+ <Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
68
+ )}
78
69
 
79
- {columns[ResultColumnId.VowelsCount] && (
80
- <Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
81
- )}
70
+ {columns[ResultColumnId.VowelsCount] && (
71
+ <Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
72
+ )}
82
73
 
83
- {columns[ResultColumnId.ConsonantsCount] && (
84
- <Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
85
- )}
74
+ {columns[ResultColumnId.ConsonantsCount] && (
75
+ <Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
76
+ )}
86
77
 
87
- {columns[ResultColumnId.BlanksCount] && (
88
- <Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
89
- )}
78
+ {columns[ResultColumnId.BlanksCount] && (
79
+ <Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
80
+ )}
90
81
 
91
- {columns[ResultColumnId.WordsCount] && (
92
- <Cell
93
- className={styles.stat}
94
- translationKey="common.words"
95
- tooltip={`${result.wordsCount.toLocaleString(locale)} (${words.join(separator)})`}
96
- value={result.wordsCount}
97
- />
98
- )}
82
+ {columns[ResultColumnId.WordsCount] && (
83
+ <Cell
84
+ className={styles.stat}
85
+ translationKey="common.words"
86
+ tooltip={`${result.wordsCount.toLocaleString(locale)} (${words.join(separator)})`}
87
+ value={result.wordsCount}
88
+ />
89
+ )}
99
90
 
100
- {columns[ResultColumnId.Points] && (
101
- <Cell className={styles.points} dataTestId="points" translationKey="common.points" value={result.points} />
102
- )}
103
- </span>
104
- </button>
91
+ {columns[ResultColumnId.Points] && (
92
+ <Cell className={styles.points} data-testid="points" translationKey="common.points" value={result.points} />
93
+ )}
94
+ </Row>
105
95
  );
106
96
  };
@@ -1,7 +1,5 @@
1
1
  @use 'styles/mixins' as *;
2
2
 
3
- $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
4
-
5
3
  .results {
6
4
  position: relative;
7
5
  display: flex;
@@ -37,122 +35,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
37
35
  }
38
36
  }
39
37
 
40
- .header {
41
- position: relative;
42
- z-index: 1;
43
- display: flex;
44
- align-items: center;
45
- justify-content: space-between;
46
- border-bottom: var(--border);
47
- border-top-left-radius: inherit;
48
- border-top-right-radius: inherit;
49
- }
50
-
51
- .headerButton {
52
- @include button-reset;
53
- @include focus-effect;
54
-
55
- cursor: pointer;
56
-
57
- text-transform: uppercase;
58
- transition:
59
- background-color var(--transition--duration) var(--transition--easing),
60
- color var(--transition--duration) var(--transition--easing);
61
- background-color: var(--color--background);
62
- height: 100%;
63
-
64
- &:first-child {
65
- border-start-start-radius: inherit;
66
- }
67
-
68
- &:last-child {
69
- border-start-end-radius: inherit;
70
- }
71
-
72
- &:focus,
73
- &:hover {
74
- background-color: var(--color--primary);
75
- color: var(--color--primary--opposite);
76
- }
77
- }
78
-
79
- .headerButtonLabel {
80
- @include ellipsis;
81
-
82
- flex: 0 1 auto;
83
- text-align: start;
84
- }
85
-
86
- .headerButtonIcon {
87
- flex: 0 0 auto;
88
- width: var(--results--icon--size);
89
- height: var(--results--icon--size);
90
- }
91
-
92
- .result {
93
- @include button-reset;
94
-
95
- user-select: none;
96
- cursor: pointer;
97
- font-weight: 400;
98
-
99
- &:focus,
100
- &:hover,
101
- &.highlighted {
102
- &:not(:disabled) {
103
- background-color: var(--color--primary);
104
- color: var(--color--primary--opposite);
105
- }
106
- }
107
-
108
- &[aria-hidden='true'] {
109
- color: var(--color--inactive);
110
-
111
- &:focus,
112
- &:hover,
113
- &.highlighted {
114
- &:not(:disabled) {
115
- background-color: var(--color--primary--light);
116
- color: var(--color--primary--opposite);
117
- }
118
- }
119
- }
120
- }
121
-
122
- .resultContent {
123
- display: flex;
124
- align-items: center;
125
- justify-content: space-between;
126
- height: 100%;
127
-
128
- .word {
129
- @include ellipsis;
130
-
131
- display: flex;
132
- }
133
- }
134
-
135
- .cell {
136
- display: flex;
137
- align-items: center;
138
- justify-content: center;
139
- height: 100%;
140
- gap: var(--spacing--s);
141
- line-height: var(--results--item--height);
142
-
143
- .result &.word,
144
- .headerButton.word & {
145
- justify-content: flex-start;
146
- padding-inline-start: $row-padding-horizontal;
147
- }
148
- }
149
-
150
- .word {
151
- flex: 1 1 0;
152
- min-width: 0;
153
- text-transform: uppercase;
154
- }
155
-
156
38
  .stat {
157
39
  $width: 55px;
158
40
 
@@ -181,14 +63,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
181
63
  margin: var(--spacing--xl) auto 0;
182
64
  }
183
65
 
184
- .sortIcon {
185
- $size: 19px;
186
-
187
- flex: 0 0 auto;
188
- width: $size;
189
- height: $size;
190
- }
191
-
192
66
  .input {
193
67
  border-top: var(--border);
194
68
  border-bottom-left-radius: var(--border--radius);
@@ -1,27 +1,17 @@
1
1
  // Generated by scripts/generate-scss-types.ts - do not edit.
2
2
  declare const styles: {
3
- readonly cell: string;
4
3
  readonly content: string;
5
4
  readonly coordinates: string;
6
5
  readonly emptyState: string;
7
- readonly header: string;
8
- readonly headerButton: string;
9
- readonly headerButtonIcon: string;
10
- readonly headerButtonLabel: string;
11
6
  readonly highlight: string;
12
- readonly highlighted: string;
13
7
  readonly input: string;
14
8
  readonly list: string;
15
9
  readonly listContainer: string;
16
10
  readonly outdated: string;
17
11
  readonly points: string;
18
- readonly result: string;
19
- readonly resultContent: string;
20
12
  readonly results: string;
21
13
  readonly solveButton: string;
22
- readonly sortIcon: string;
23
14
  readonly stat: string;
24
- readonly word: string;
25
15
  };
26
16
 
27
17
  export default styles;
@@ -1,5 +1,6 @@
1
1
  import classNames from 'classnames';
2
- import { useEffect, useMemo, type FunctionComponent } from 'react';
2
+ import { useCallback, useEffect, useMemo, type FunctionComponent } from 'react';
3
+ import { useDispatch } from 'react-redux';
3
4
  import { List, useListRef } from 'react-window';
4
5
  import { useDebounce } from 'use-debounce';
5
6
 
@@ -7,9 +8,11 @@ import { useLatest } from '@/hooks/useLatest';
7
8
  import { LOCALE_FEATURES } from '@/i18n/constants';
8
9
  import { RESULTS_ITEM_HEIGHT, RESULTS_OVERSCAN_COUNT } from '@/parameters';
9
10
  import {
11
+ resultsSlice,
10
12
  selectAreResultsOutdated,
11
13
  selectLocale,
12
14
  selectProcessedResults,
15
+ selectResultsQuery,
13
16
  selectSolveError,
14
17
  selectSolveIsLoading,
15
18
  useTranslate,
@@ -18,7 +21,7 @@ import {
18
21
 
19
22
  import { EmptyState } from '../EmptyState';
20
23
  import { Loading } from '../Loading';
21
- import { ResultsInput } from '../ResultsInput';
24
+ import { Search } from '../Table';
22
25
 
23
26
  import { Header } from './Header';
24
27
  import { Result } from './Result';
@@ -35,10 +38,12 @@ interface Props {
35
38
  const IS_LOADING_DEBOUNCE = 100;
36
39
 
37
40
  export const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIndex }) => {
41
+ const dispatch = useDispatch();
38
42
  const translate = useTranslate();
39
43
  const locale = useTypedSelector(selectLocale);
40
44
  const { direction } = LOCALE_FEATURES[locale];
41
45
  const results = useTypedSelector(selectProcessedResults);
46
+ const query = useTypedSelector(selectResultsQuery);
42
47
  const isLoading = useTypedSelector(selectSolveIsLoading);
43
48
  const [isLoadingDebounced] = useDebounce(isLoading, IS_LOADING_DEBOUNCE);
44
49
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
@@ -58,7 +63,14 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
58
63
  const scrollToIndex = typeof highlightedIndex === 'number' ? highlightedIndex : 0;
59
64
  const scrollToIndexRef = useLatest(scrollToIndex);
60
65
  const hasResults = typeof error === 'undefined' && typeof results !== 'undefined';
61
- const showInput = hasResults && results.length > 0 && !isOutdated;
66
+ const showSearch = hasResults && results.length > 0 && !isOutdated;
67
+
68
+ const handleQueryChange = useCallback(
69
+ (newQuery: string) => {
70
+ dispatch(resultsSlice.actions.changeQuery(newQuery));
71
+ },
72
+ [dispatch],
73
+ );
62
74
 
63
75
  useEffect(() => {
64
76
  // without setTimeout, the initial scrolling offset is calculated
@@ -119,7 +131,7 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
119
131
  )}
120
132
 
121
133
  {!isOutdated && results.length > 0 && (
122
- <div className={styles.listContainer} onMouseLeave={callbacks.onMouseLeave}>
134
+ <div className={styles.listContainer}>
123
135
  <List
124
136
  className={classNames(styles.list, {
125
137
  [styles.outdated]: isOutdated,
@@ -131,6 +143,7 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
131
143
  rowCount={results.length}
132
144
  rowHeight={RESULTS_ITEM_HEIGHT}
133
145
  rowProps={itemData}
146
+ onMouseLeave={callbacks.onMouseLeave}
134
147
  />
135
148
  </div>
136
149
  )}
@@ -138,7 +151,14 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
138
151
  )}
139
152
  </div>
140
153
 
141
- {showInput && <ResultsInput className={styles.input} />}
154
+ {showSearch && (
155
+ <Search
156
+ className={styles.input}
157
+ placeholder={translate('results.input.placeholder')}
158
+ value={query}
159
+ onChange={handleQueryChange}
160
+ />
161
+ )}
142
162
 
143
163
  {isLoadingDebounced && <Loading />}
144
164
  </div>
@@ -8,7 +8,7 @@
8
8
  display: flex;
9
9
  flex-direction: column;
10
10
 
11
- @include media('<l', 'touch') {
11
+ @include media('compact', 'touch') {
12
12
  margin-bottom: calc(3 * var(--spacing--l) + var(--button--icon--size));
13
13
  }
14
14
 
@@ -31,7 +31,7 @@
31
31
  height: 100%;
32
32
  margin: 0 auto;
33
33
 
34
- @include media('<l') {
34
+ @include media('compact') {
35
35
  grid-template-columns: 1fr;
36
36
  }
37
37
  }
@@ -54,7 +54,7 @@
54
54
  flex: 0 0 var(--solver-column--width);
55
55
  gap: var(--spacing);
56
56
 
57
- @include media('<l') {
57
+ @include media('compact') {
58
58
  display: none;
59
59
  }
60
60
  }
@@ -106,7 +106,7 @@
106
106
  width: 100%;
107
107
  max-width: var(--controls-max-width);
108
108
 
109
- @include media('>=l') {
109
+ @include media('desktop') {
110
110
  display: none;
111
111
  }
112
112
  }
@@ -3,13 +3,14 @@ import classNames from 'classnames';
3
3
  import { type FunctionComponent, memo, type SyntheticEvent, useEffect, useMemo } from 'react';
4
4
  import { useDispatch } from 'react-redux';
5
5
 
6
+ import { useIsCompactLayout } from '@/hooks/useIsCompactLayout';
6
7
  import { useIsTouchDevice } from '@/hooks/useIsTouchDevice';
7
- import { useMediaQuery } from '@/hooks/useMediaQuery';
8
8
  import {
9
9
  resultsSlice,
10
10
  selectAreResultsOutdated,
11
11
  selectProcessedResults,
12
12
  selectResultCandidate,
13
+ selectResultCandidateIndex,
13
14
  selectSolveError,
14
15
  solveSlice,
15
16
  useTranslate,
@@ -36,10 +37,11 @@ const SolverBase: FunctionComponent<Props> = ({ className, onShowResults }) => {
36
37
  const dispatch = useDispatch();
37
38
  const translate = useTranslate();
38
39
  const isTouchDevice = useIsTouchDevice();
39
- const showCompactControls = useMediaQuery('<l');
40
+ const showCompactControls = useIsCompactLayout();
40
41
  const error = useTypedSelector(selectSolveError);
41
42
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
42
43
  const results = useTypedSelector(selectProcessedResults);
44
+ const highlightedIndex = useTypedSelector(selectResultCandidateIndex);
43
45
  const store = useTypedStore();
44
46
  const [bestResult] = results || [];
45
47
  const touchCallbacks = useMemo(
@@ -100,7 +102,7 @@ const SolverBase: FunctionComponent<Props> = ({ className, onShowResults }) => {
100
102
  </form>
101
103
 
102
104
  <div className={styles.column}>
103
- <Results callbacks={callbacks} className={styles.results} />
105
+ <Results callbacks={callbacks} className={styles.results} highlightedIndex={highlightedIndex} />
104
106
 
105
107
  <div data-testid="dictionary" className={styles.dictionaryContainer}>
106
108
  <Dictionary className={styles.dictionary} />
@@ -96,7 +96,6 @@
96
96
  flex: 1;
97
97
  text-transform: uppercase;
98
98
  text-align: center;
99
- white-space: pre-wrap;
100
99
 
101
100
  @include media('<xs') {
102
101
  padding: var(--spacing--m);
@@ -104,6 +103,11 @@
104
103
  }
105
104
  }
106
105
 
106
+ .placeholder {
107
+ color: var(--color--foreground--secondary);
108
+ text-transform: none;
109
+ }
110
+
107
111
  .iconContainer {
108
112
  flex: 0 0 auto;
109
113
  display: flex;
@@ -8,6 +8,7 @@ declare const styles: {
8
8
  readonly iconContainer: string;
9
9
  readonly insert: string;
10
10
  readonly loading: string;
11
+ readonly placeholder: string;
11
12
  readonly points: string;
12
13
  readonly resultCandidate: string;
13
14
  readonly resultCandidatePicker: string;
@@ -35,7 +35,7 @@ export const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onR
35
35
  const results = useTypedSelector(selectProcessedResults);
36
36
  const resultCandidate = useTypedSelector(selectResultCandidate);
37
37
  const index = resultCandidate && results ? results.findIndex((result) => result.id === resultCandidate.id) : -1;
38
- const disabled = isOutdated || !resultCandidate;
38
+ const disabled = isOutdated || !results || results.length === 0;
39
39
  const isPreviousDisabled = !results || index <= 0 || disabled;
40
40
  const isNextDisabled = !results || index >= results.length - 1 || disabled;
41
41
  const bothEnabled = !isPreviousDisabled && !isNextDisabled;
@@ -77,6 +77,7 @@ export const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onR
77
77
  <button
78
78
  aria-label={translate('results')}
79
79
  className={styles.resultCandidate}
80
+ data-testid="result-candidate-picker"
80
81
  disabled={disabled}
81
82
  type="button"
82
83
  onClick={onResultClick}
@@ -88,7 +89,9 @@ export const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onR
88
89
  </>
89
90
  )}
90
91
 
91
- {!resultCandidate && <div className={styles.word}> </div>}
92
+ {!resultCandidate && (
93
+ <div className={classNames(styles.word, styles.placeholder)}>{translate('results.select')}</div>
94
+ )}
92
95
 
93
96
  <div className={styles.iconContainer}>
94
97
  {isLoading && <Spinner className={styles.loading} />}
@@ -0,0 +1,42 @@
1
+ import classNames from 'classnames';
2
+ import { type FunctionComponent, type ReactNode } from 'react';
3
+
4
+ import { selectLocale, useTranslate, useTypedSelector } from '@/state';
5
+ import { type TranslationKey } from '@/types';
6
+
7
+ import { Tooltip } from '../Tooltip';
8
+
9
+ import styles from './Table.module.scss';
10
+
11
+ interface Props {
12
+ children?: ReactNode;
13
+ className?: string;
14
+ 'data-testid'?: string;
15
+ primary?: boolean;
16
+ translationKey?: TranslationKey;
17
+ tooltip?: string | number;
18
+ value: string | number;
19
+ }
20
+
21
+ export const Cell: FunctionComponent<Props> = ({
22
+ children,
23
+ className,
24
+ 'data-testid': dataTestId,
25
+ primary,
26
+ translationKey,
27
+ tooltip,
28
+ value,
29
+ }) => {
30
+ const translate = useTranslate();
31
+ const locale = useTypedSelector(selectLocale);
32
+ const formattedValue = value.toLocaleString(locale);
33
+ const label = translationKey ? `${translate(translationKey)}: ` : '';
34
+
35
+ return (
36
+ <Tooltip tooltip={`${label}${tooltip || formattedValue}`}>
37
+ <div className={classNames(styles.cell, className, { [styles.primary]: primary })} data-testid={dataTestId}>
38
+ <span className={styles.cellValue}>{children || formattedValue}</span>
39
+ </div>
40
+ </Tooltip>
41
+ );
42
+ };
@@ -0,0 +1,11 @@
1
+ import { type FunctionComponent, type ReactNode } from 'react';
2
+
3
+ import styles from './Table.module.scss';
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ }
8
+
9
+ export const Header: FunctionComponent<Props> = ({ children }) => {
10
+ return <div className={styles.header}>{children}</div>;
11
+ };
@@ -1,41 +1,49 @@
1
1
  import classNames from 'classnames';
2
2
  import { type FunctionComponent, type ReactElement, type SVGAttributes, useCallback } from 'react';
3
- import { useDispatch } from 'react-redux';
4
3
 
5
4
  import SortDown from '@/icons/SortDown.svg';
6
5
  import SortUp from '@/icons/SortUp.svg';
7
- import { resultsSlice, selectResultsSort, useTranslate, useTypedSelector } from '@/state';
8
- import { type ResultColumnId, SortDirection, type TranslationKey } from '@/types';
6
+ import { useTranslate } from '@/state';
7
+ import { type Sort, SortDirection, type TranslationKey } from '@/types';
9
8
 
10
9
  import { Tooltip } from '../Tooltip';
11
10
 
12
- import styles from './Results.module.scss';
11
+ import styles from './Table.module.scss';
13
12
 
14
- interface Props {
15
- className: string;
13
+ interface Props<Id extends string> {
14
+ className?: string;
16
15
  Icon?: FunctionComponent<SVGAttributes<SVGElement>>;
17
- id: ResultColumnId;
16
+ id: Id;
17
+ primary?: boolean;
18
+ sort: Sort<Id>;
18
19
  translationKey: TranslationKey;
20
+ onSort: (id: Id) => void;
19
21
  }
20
22
 
21
- export const HeaderButton = ({ className, Icon, id, translationKey }: Props): ReactElement => {
22
- const dispatch = useDispatch();
23
+ export const HeaderButton = <Id extends string>({
24
+ className,
25
+ Icon,
26
+ id,
27
+ primary,
28
+ sort,
29
+ translationKey,
30
+ onSort,
31
+ }: Props<Id>): ReactElement => {
23
32
  const translate = useTranslate();
24
- const sort = useTypedSelector(selectResultsSort);
25
33
 
26
34
  const handleClick = useCallback(() => {
27
- dispatch(resultsSlice.actions.sort(id));
28
- }, [dispatch, id]);
35
+ onSort(id);
36
+ }, [id, onSort]);
29
37
 
30
38
  return (
31
39
  <Tooltip tooltip={translate(translationKey)}>
32
40
  <button
33
41
  aria-label={translate(translationKey)}
34
- className={classNames(styles.headerButton, className)}
42
+ className={classNames(styles.headerButton, className, { [styles.primary]: primary })}
35
43
  type="button"
36
44
  onClick={handleClick}
37
45
  >
38
- <span className={styles.cell}>
46
+ <span className={classNames(styles.cell, { [styles.primary]: primary })}>
39
47
  {Icon && <Icon aria-hidden="true" className={styles.headerButtonIcon} role="img" />}
40
48
 
41
49
  {!Icon && <span className={styles.headerButtonLabel}>{translate(translationKey)}</span>}